iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
Modern Web

轉生成前端工程師後,30步離開新手村!系列 第 29

# 使用Cypress為Angular專案撰寫整合測試特性的E2E測試

  • 分享至 

  • xImage
  •  

前言

今天我們要聊聊工作中遇到的一個問題。我們需要撰寫測試來驗證前端需求的正確性(也就是UI/UX)。於是我們想,E2E測試非常適合,但是因為環境和經費種種因素,該測試必須要在地端和隔離外部的狀況下執行。所以最終想到的解法就是使用E2E測試的做法,搭配整合測試的環境來進行。


為什麼叫整合測試特性的E2E測試

首先,端對端測試(End-to-End Testing,E2E測試)旨在驗證應用程式的完整流程,確保從使用者的角度來看,所有組件和系統之間的互動都能正常運作。E2E測試應涵蓋整個應用程式的工作流程,包括從前端到後端,乃至與外部系統或第三方服務的整合。

而整合測試(Integration Testing)旨在驗證不同模組或組件之間的相互運作是否正確。當這些模組整合在一起時,透過進行整合測試來檢查它們在協同運作時是否如預期般正常。

而需要使用使用者角度進行驗證,但是需要隔離外部系統的測試,我們就專而將目標定義在兩者之間,也就是以使用者的角度驗證組件之間的交互是否正確。也因此稱為整合測試特性的E2E測試。

針對這樣的測試情境,我們決定使用Angular的 Core / Shared / Feature 專案架構,搭配Cypress,在模擬後端和第三方資料的情況下。在地端環境進行以頁面為單位使用者操作為基準的測試。

那麼讓我們開始吧!


Angular的 Core Shared Feature 架構

我們將Angular專案中的檔案依照業務性質不同來分類到三個模組中。目的是讓程式碼更具組織性、可維護性和可重用性。

1. Core

  • 核心職責:Core 模組通常包含應用程式中 單一實例(singleton) 的服務和全域(global)的功能,如 身份驗證服務全域導航守衛應用程式初始化邏輯
  • 範例內容
    • 認證、授權服務(Authentication/Authorization)
    • HTTP 攔截器
    • 全局配置(例如環境設定檔)
    • 根導航守衛(Root Guards)

2. Shared

  • 核心職責:Shared 模組用來存放 可重用的組件、指令、管道和服務。這些功能不屬於任何一個特定的業務功能,但可以在整個應用中重複使用。
  • 範例內容
    • 共用的 UI 組件(如按鈕、表單元件)
    • 常見的管道(Pipes,如貨幣格式化)
    • 通用的指令(Directives)
    • 可在多處使用的服務(例如彈窗服務)

3. Feature

  • 核心職責:Feature 模組是應用程式的業務邏輯或功能區塊。每個 Feature 模組通常代表應用中的一個具體業務功能,如「使用者管理」、「產品管理」等。
  • 範例內容
    • 使用者管理模組(User Management Module)
    • 產品管理模組(Product Management Module)
    • 訂單系統模組(Order System Module)
  • 注意事項:每個 Feature 模組通常會有自己的組件、服務和路由,並且它們只會在需要的時候被載入,以實現按需加載(Lazy Loading)。

簡而言之,Core負責擺放系統運作所需要的全域單一實例服務,Shared負責擺放與業務無關,需要複用的原件或是服務,Feature負責擺放業務邏輯的實作。

ecommerce-app/
├── src/
│   ├── app/
│   │   ├── core/
│   │   │   ├── services/
│   │   │   │   ├── auth.service.ts
│   │   │   │   ├── error-handler.service.ts
│   │   │   │   └── api-interceptor.service.ts
│   │   │   └── app.component.ts         <-- 應用程式的入口組件,註冊全局服務
│   │   │
│   │   ├── shared/
│   │   │   ├── components/
│   │   │   │   ├── button.component.ts  <-- 共用按鈕組件
│   │   │   ├── pipes/
│   │   │   │   └── currency-format.pipe.ts  <-- 貨幣格式化管道
│   │   │   └── directives/
│   │   │       └── example.directive.ts <-- 共用指令 (如果需要)
│   │   │
│   │   ├── features/
│   │   │   ├── products/
│   │   │   │   ├── product-list/
│   │   │   │   │   ├── product-list.component.ts   <-- 商品列表組件
│   │   │   │   │   └── product-list.component.html
│   │   │   │   ├── product-details/
│   │   │   │   │   ├── product-details.component.ts   <-- 商品詳情組件
│   │   │   │   │   └── product-details.component.html
│   │   │   │   └── product.service.ts   <-- 商品相關的服務
│   │   │   ├── user/
│   │   │   │   ├── user-profile/
│   │   │   │   │   ├── user-profile.component.ts  <-- 使用者資料組件
│   │   │   │   │   └── user-profile.component.html
│   │   │   │   └── user.service.ts   <-- 使用者相關服務
│   │   │   └── cart/
│   │   │       ├── cart.component.ts  <-- 購物車組件
│   │   │       └── cart.service.ts    <-- 購物車服務
│   │   │
│   │   ├── app-routing.module.ts  <-- 應用程式路由配置,包含 lazy loading
│   │   └── main.ts                <-- 應用程式的啟動點
│   │
│   ├── assets/                    <-- 靜態資源 (圖片、字體等)
│   └── environments/              <-- 環境設定檔 (開發、測試、產品環境)
│       ├── environment.ts
│       └── environment.prod.ts

Cypress 測試框架

這部分讓我們邀請ChatGpt為我們進行說明,

Me: 什麼是Cypress?

ChatGpt: Cypress 是一個現代化的前端測試工具,專門用來自動化測試網頁應用程式。它主要用於進行端對端測試(End-to-End testing)、單元測試(Unit testing)和整合測試(Integration testing)。Cypress 的設計目的是解決傳統網頁測試工具的一些缺點,如複雜的配置、速度慢以及不穩定的測試結果。

接著我們在專案中導入Cypress。

其實Cypress的安裝超級無敵簡單,官方網站的說明非常詳細易懂,下方我們快速上步驟。

  1. 安装 Cypress
npm install cypress --save-dev
  1. 初始化 Cypress

我們可以透過

npx cypress open

的指令快速建立資料夾結構。

cypress/
  └── e2e/
  └── fixtures/
  └── plugins/
  └── support/
  1. 設定 cypress.config.json
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:4200',
  },
  component: {
    devServer: {
      framework: 'angular',
      bundler: 'webpack',
    },
    specPattern: '**/*.cy.ts',
  }
});
  1. 更新 package.json 中的 script
"scripts": {
  "cypress:open": "cypress open",
  "cypress:run": "cypress run"
}

這樣我們可以直接透過 npm 來啟動Cypress。

npm run cypress:open
  1. 安裝 mochawesome 和 JUnit Reporter 作為Cypress的報告產生器
npm install cypress-multi-reporters mocha-junit-reporter mochawesome --save-dev
  1. 更新 cypress.config.json 使用 reporter
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:4200',
  },
  component: {
    devServer: {
      framework: 'angular',
      bundler: 'webpack',
    },
    specPattern: '**/*.cy.ts',
  },
  reporter: 'mocha-junit-reporter',
  reporterOptions: {
    mochaFile: 'cypress/results/junit-[hash].xml',
    toConsole: true,
  },
});

到此我們的環境就裝好了,下一步就是撰寫測試了。


解決方案設計

首先我們的Cypress結構會分成四個資料夾,如下:

cypress/
├── e2e/
├── fixtures/
├── pom/
├── support/
└── tsconfig.json
  1. e2e資料夾負責存放所有的測試案例,並且測試案例的擺放會比照專案中的feature資料夾,以此來達成每一個頁面一個測試檔案。舉例來說,我們的專案的features如下,有三個feature,四個頁面。
│   │   ├── features/
│   │   │   ├── products/
│   │   │   │   ├── product-list/
│   │   │   │   │   ├── product-list.component.ts   <-- 商品列表組件
│   │   │   │   │   └── product-list.component.html
│   │   │   │   ├── product-details/
│   │   │   │   │   ├── product-details.component.ts   <-- 商品詳情組件
│   │   │   │   │   └── product-details.component.html
│   │   │   │   └── product.service.ts   <-- 商品相關的服務
│   │   │   ├── user/
│   │   │   │   ├── user-profile/
│   │   │   │   │   ├── user-profile.component.ts  <-- 使用者資料組件
│   │   │   │   │   └── user-profile.component.html
│   │   │   │   └── user.service.ts   <-- 使用者相關服務
│   │   │   └── cart/
│   │   │       ├── cart.component.ts  <-- 購物車組件
│   │   │       └── cart.service.ts    <-- 購物車服務

測試專案就會長的如下。

cypress/
├── e2e/
│   └── feature/
│       ├── products/
│       │   ├── product-list.cy.ts
│       │   └── product-details.cy.ts
│       ├── user/
│       │   └── user-profile.cy.ts
│       └── cart/
│           └── cart.cy.ts
├── fixtures/
├── pom/
├── support/
└── tsconfig.json

  1. fixtures資料夾負責擺放所有的假資料,假資料為json格式,且帶有 .data 的後墜。例如: Token.data.json 。並且若資料有正常異常等情境上的差異會再加入情境的後墜。例如: Token.normal.data.json 。假資料是所有測試共用的,以我們的解決方案來說,會用到假資料的時機就是要隔離外部服務的時候,所以第三方服務和後端API等都會使用fixture中的資料進行模擬,也因此,我們會使用API做為拆分fixture的力度。
  2. pom資料夾負責擺放 Page Object Model ,這個設計模式會將每一個頁面封裝成物件,物件中的內容會是針對該頁面進行操作的共用cypress語法。透過這個方式進行管理,也減少每一次需求調整後要改動的測試量。
  3. support資料夾通常會包含一些輔助性的設置或自定義的命令,跟pom主要的區別在,support中的共用命令是沒有特定業務邏輯的,也可以理解成是全域共用的。

接著是測案撰寫規範。一個測案中一樣分成兩層的 describe

第一層 describe 用於描述該測試頁面,第二層的 describe 則依照使用者跟該畫面的互動進行拆分,而其中的每一個 it 則代表了該操作的各種情境。

而測試檔案也與單元測試非常類似,分成:

  1. 設定與初始化區塊
  2. 測試區塊
  3. 清理區塊

為什麼沒有mock區塊呢? 因為所有的內容已經被封裝到POM裡面了,所以在測試檔案中,我們只需實作POM並且調用其中的方法即可。這個方式也能夠確保測試案例中只有測試的邏輯和操作。


實作演練

我們以 product-list.component.ts 做為範例來實作一次。

  1. 首先建立該component的 POM
// pom/feature/product-list/product-list.pom.ts
export class PomProductList {
    visit() {
        cy.visit('/product-list');  // 假設路徑是 /product-list
    }

    getProductItems() {
        return cy.get('[data-cy="product-item"]');  // 使用 data-cy 埋點選取產品項目
    }

    getAddToCartButton(productName: string) {
        return cy.contains('[data-cy="product-item"]', productName).find('[data-cy="add-to-cart"]');
    }

    clickAddToCartButton(productName: string) {
        this.getAddToCartButton(productName).click();
    }

    getCartItemCount() {
        return cy.get('[data-cy="cart-item-count"]');  // 使用 data-cy 埋點選取購物車項目數量
    }
}

將所有與此頁面元件有關,或是需要mock的項目實作在POM中。

  1. 實作product-list.cy.ts
// e2e/feature/product-list/product-list.cy.ts
import { ProductListPage } from '../../../pom/feature/product-list/ProductListPage';

describe('ProductList', () => {
		// 實作建立好的POM
    const productListPage = new ProductListPage();

    beforeEach(() => {
        productListPage.visit();  // 在每次測試前都進入產品列表頁面
    });

    // 描述使用者操作 "添加產品到購物車"
    describe('Add Product to Cart', () => {
        it('should allow the user to add a product to the cart', () => {
		        // Arrange
            // 模擬添加一個產品到購物車
            const productName = 'Sample Product';  // 假設產品名稱
            
            // Act
            productListPage.clickAddToCartButton(productName);
            
            // Assert
            // 檢查購物車中是否顯示產品
            productListPage.getCartItemCount().should('contain', '1');
        });
    });

    // 描述使用者操作 "檢查產品顯示"
    describe('Display Products', () => {
        it('should display multiple products', () => {
            // Arrange
            // Act
            // Assert
            // 檢查是否顯示多個產品項目
            productListPage.getProductItems().should('have.length.greaterThan', 0);
        });
    });
});

第一層 describe 定義了本測試檔案的範圍是 ProductList 頁面,接著實作了我們要驗證的頁面POM,以調用與此 component 互動的方法。接著在每一個測案執行前,都先前往該頁面。接著第二層 describe 依照使用者會與此頁面互動的行為進行拆分。通常可將行為分成兩種,第一種是進入頁面後就會觸發的初始化行為,而其他則是由使用者操作觸發的操作行為。最後每一個 it 測試案例使用 AAA Pattern 進行撰寫。其實到這邊會發現,進行如此拆分後的測試變的非常容易撰寫,需要資源、模擬回傳、取得DOM等等的操作,全部到POM找,所有測試都可以專注在業務情境和測試邏輯。當然實務層面不會像範例這麼簡單,不過底層的設計想法和分工都在上面了,基本上都可以找到對應的分類。


到這邊就是我們解決方案的內容了,希望這樣 Angular Core / Shared / Feature,搭配 Cypress + POM設計模式 + AAA Pattern 的方式能夠幫助到有類似痛點的朋友。

而最後一篇文章我們就分享一下,如何將測試導入Azure的ci pipeline中,讓整個流程全部串接起來!


上一篇
# 使用Jest為Angular專案撰寫UnitTest(三)
下一篇
# 在Azure CI Pipeline中運行前端測試
系列文
轉生成前端工程師後,30步離開新手村!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言